package pacman;

import static pacman.game.Constants.DELAY;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Random;


import pacman.controllers.Controller;
import pacman.controllers.examples.AggressiveGhosts;
import pacman.controllers.examples.Legacy;
import pacman.controllers.examples.NearestPillPacMan;
import pacman.controllers.examples.NearestPillPacManVS;
import pacman.controllers.examples.RandomNonRevPacMan;
import pacman.controllers.examples.RandomPacMan;
import pacman.controllers.examples.StarterGhosts;
import pacman.controllers.examples.StarterPacMan;
import pacman.entries.pacman.*;
import pacman.entries.pacman.ml.TrainablePacMan;
import pacman.game.Game;
import pacman.game.GameView;
import pacman.game.Constants.GHOST;
import pacman.game.Constants.MOVE;

public class ExecutorDiagnostics extends Executor {
	
	private static List<Statistic> StandardStatistics() {
		List<Statistic> statistics = new ArrayList<Statistic>(10);
		statistics.add(new ScoreStatistic());
		statistics.add(new PillStatistic());
		statistics.add(new PowerPillStatistic());
		statistics.add(new NumberGhostsEatenStatistic());
		statistics.add(new TimePerDeathStatistic());
		statistics.add(new TicksPerGameStatistic());
		statistics.add(new TicksPerLevelStatistic());
		statistics.add(new LevelsReachedStatistic());
		statistics.add(new PacManTimeoutRatePerTurnStatistic());
		statistics.add(new PacManAverageTurnTimeStatistic());
		return statistics;
	}
	
	static ExecutorDiagnostics exec=new ExecutorDiagnostics();
	
	/**
	 * Run diagnostics on the AIs.  
	 *
	 * @param args the command line arguments
	 */
	public static void main(String[] args) {

		//run multiple games in batch mode - good for testing.
		// int numTrials=1000;
		
		//exec.runDiagnostics(new StarterPacMan(),new StarterGhosts(),statistics,numTrials,System.out);
		
		// Run diagnostics for many controllers
		LinkedList<TestPair> testPairs = new LinkedList<ExecutorDiagnostics.TestPair>();
		
		/*
		// StarterPacMan
		//testPairs.add(exec.new TestPair(new StarterPacMan(), "StarterPacMan", new StarterGhosts(), "StarterGhosts"));
		//testPairs.add(exec.new TestPair(new StarterPacMan(), "StarterPacMan", new Legacy(), "LegacyGhosts"));
		//testPairs.add(exec.new TestPair(new StarterPacMan(), "StarterPacMan", new AggressiveGhosts(), "AggressiveGhosts"));
		
		// MyPacMan
		testPairs.add(exec.new TestPair(new MyPacMan(), "MyPacMan", new StarterGhosts(), "StarterGhosts"));
		testPairs.add(exec.new TestPair(new MyPacMan(), "MyPacMan", new Legacy(), "LegacyGhosts"));
		testPairs.add(exec.new TestPair(new MyPacMan(), "MyPacMan", new AggressiveGhosts(), "AggressiveGhosts"));

		// PerceptronPacMan
		testPairs.add(exec.new TestPair(new PerceptronPacMan(), "PerceptronPacMan", new StarterGhosts(), "StarterGhosts"));
		testPairs.add(exec.new TestPair(new PerceptronPacMan(), "PerceptronPacMan", new Legacy(), "LegacyGhosts"));
		testPairs.add(exec.new TestPair(new PerceptronPacMan(), "PerceptronPacMan", new AggressiveGhosts(), "AggressiveGhosts"));

		// LearningPacMan
		//testPairs.add(exec.new TestPair(new LearningPacMan(), "LearningPacMan", new StarterGhosts(), "StarterGhosts"));
		//testPairs.add(exec.new TestPair(new LearningPacMan(), "LearningPacMan", new Legacy(), "LegacyGhosts"));
		//testPairs.add(exec.new TestPair(new LearningPacMan(), "LearningPacMan", new AggressiveGhosts(), "AggressiveGhosts"));
		
		exec.runDiagnostics(testPairs, StandardStatistics(), 100);
		*/
		
		/*
		// compare the difference between two pac man controllers for debugging
		StarterGhosts sghost1 = new StarterGhosts();
		StarterGhosts sghost2 = new StarterGhosts();
		//Random rnd = new Random(0);
		//long randSeed = rnd.nextLong();
		sghost1.rnd.setSeed(0);
		sghost2.rnd.setSeed(0);
		exec.runComparison(exec.new TestPair(new PerceptronPacMan(),      "PerceptronPacMan",      sghost1, "StarterGhosts")
			             , exec.new TestPair(new PerceptronPacManNotOO(), "PerceptronPacManNotOO", sghost2, "StarterGhosts")
						 , 0, false );
		*/
		
		GeneticPacMan3 pacMan3 = new GeneticPacMan3();
		pacMan3.LoadWeights("geneticTrainedWeights320x12");
		pacMan3.PrintWeights();
		
		//exec.TrainReinforcementLearner();
		/*LearningPacMan learningPacMan = TrainGeneticLearner();
		//LearningPacMan learningPacMan = new LearningPacMan();
		//learningPacMan.LoadWeights("Genetic2Weights");
		
		testPairs.add(exec.new TestPair(learningPacMan, "GeneticPacMan3", new StarterGhosts(), "StarterGhosts"));
		testPairs.add(exec.new TestPair(learningPacMan, "GeneticPacMan3", new Legacy(), "LegacyGhosts"));
		testPairs.add(exec.new TestPair(learningPacMan, "GeneticPacMan3", new AggressiveGhosts(), "AggressiveGhosts"));
		exec.runDiagnostics(testPairs, StandardStatistics(), 1000);
		
		//trainedLearningPacMan.SaveWeights("geneticTrainedWeights3");*/

		/*testPairs.add(exec.new TestPair(new StarterPacMan(), "StarterPacMan", new StarterGhosts(), "StarterGhosts"));
		testPairs.add(exec.new TestPair(new RandomPacMan(), "RandomPacMan", new StarterGhosts(), "StarterGhosts"));
		testPairs.add(exec.new TestPair(new RandomNonRevPacMan(), "RandomNonRevPacMan", new StarterGhosts(), "StarterGhosts"));
		testPairs.add(exec.new TestPair(new NearestPillPacMan(), "NearestPillPacMan", new StarterGhosts(), "StarterGhosts"));
		//testPairs.add(exec.new TestPair(new NearestPillPacManVS(), "StarterPacMan", new StarterGhosts(), "StarterGhosts"));
		runDiagnostics(testPairs, StandardStatistics(), 1000);*/
		
		//LearningPacMan pacMan = new LearningPacMan();
		//pacMan.LoadWeights("geneticTrainedWeights2");
		//pacMan.PrintWeights();
	}
	
	private static LearningPacMan TrainGeneticLearner() {
		
		double time = System.currentTimeMillis();
		
		int PPG = 4; // 4 // parents per generation
		float randomness = 2.0f;
		float RDF = 0.95f; // randomness decrease factor
		int numGenerations = 5; // 30
		int generationSize = 10; // 50
		
		LearningPacMan parent = new DestinationLearningPacMan();
		ArrayList<LearningPacMan> parentList = new ArrayList<LearningPacMan>(1);
		parentList.add(parent);
		
		System.out.println("Average scores for survivors of stages:");
		
		for (int g = 0; g < numGenerations-1; g++) {
			System.out.println("Pruning generation " + (g+1));
			parentList = exec.TrainGeneration(parentList, generationSize, randomness, PPG);
			randomness *= RDF;
			
			// save diagnostic
			double sum = 0.0;
			for (LearningPacMan learningPacMan : parentList) {
				List<Statistic> statistics = new ArrayList<ExecutorDiagnostics.Statistic>();
				ScoreStatistic scoreStatistic = new ScoreStatistic();
				statistics.add(scoreStatistic);
				runDiagnostics(learningPacMan, new StarterGhosts(), statistics, 100, null);
				sum += scoreStatistic.getAverage();
			}
			//parentList.get(0).SaveWeights("midTrainingWeights");
			sum /= parentList.size();
			System.out.println(g + "\t" + sum);
		}
		
		/*
		System.out.println("Evaluating best PacMen after training");
		for (LearningPacMan learningPacMan : parentList) {
			runDiagnostics(learningPacMan, new StarterGhosts(), StandardStatistics(), 5000, System.out);
		}
		*/
		
		ArrayList<LearningPacMan> bestList = exec.TrainGeneration(parentList, generationSize, randomness, 1);
		
		time = (System.currentTimeMillis()-time) / 1000.0f;
		System.out.println("Finished training after " + time + " seconds.");
		
		return bestList.get(0);
	}
	
	private ArrayList<LearningPacMan> TrainGeneration(ArrayList<LearningPacMan> parents, 
			int count, float randomness, int numBest) {
		
		ArrayList<LearningPacMan> generation = DestinationLearningPacMan.Generation(parents, count, randomness);
		
		PriorityQueue<GeneticTrainingPair> bestPerformers = new PriorityQueue<ExecutorDiagnostics.GeneticTrainingPair>();
		
		
		for (int i = 0; i < generation.size(); i++) {
			System.out.println("test start " + (i+1) + " of " + generation.size());
			LearningPacMan learningPacMan = generation.get(i);
			
			List<Statistic> statistics = new ArrayList<Statistic>(1);
			statistics.add(new ScoreStatistic());
			
			runDiagnostics(learningPacMan, new StarterGhosts(), statistics, 100, null);
			
			ScoreStatistic scoreStatistic = (ScoreStatistic)statistics.get(0);

			GeneticTrainingPair trainingPair = exec.new GeneticTrainingPair(scoreStatistic.getAverage(), learningPacMan);
			if (bestPerformers.size() < numBest)
				bestPerformers.add(trainingPair);
			else if (bestPerformers.peek().m_Score < trainingPair.m_Score) {
				bestPerformers.poll();
				bestPerformers.add(trainingPair);
			}
			System.out.println("test end");
		}
		
		ArrayList<LearningPacMan> bestLearningPacMen = new ArrayList<LearningPacMan>(bestPerformers.size());
		for (GeneticTrainingPair pair : bestPerformers)
			bestLearningPacMen.add(pair.m_LearningPacMan);
		
		return bestLearningPacMen;
	}
	
	private class GeneticTrainingPair implements Comparable<GeneticTrainingPair>
	{
		public double m_Score;
		public LearningPacMan m_LearningPacMan;
		
		public GeneticTrainingPair(double score, LearningPacMan learningPacMan) {
			m_Score = score;
			m_LearningPacMan = learningPacMan;
		}

		@Override
		public int compareTo(GeneticTrainingPair o) {
			
			double comparison = m_Score - o.m_Score;
			
			if (comparison > 0)
				return 1;
			if (comparison < 0)
				return -1;
			return 0;
		}
	}
	
	private void TrainReinforcementLearner() {
		
		int trainGames = 2000;
		int testGames = 1000;
		int trainIterations = 10;
		
		LearningPacMan learningPacMan = new LearningPacMan();
		
		int stepSize = trainGames / 40;
		int stepCount = 0;
		for (int i = 0; i < trainIterations; i++) {
			
			learningPacMan.ResetMoveCounts();
			
			System.out.print("Step " + i + ": Training on " + trainGames + " games: ");
			for (int j = 0; j < trainGames; j++) {
				runDiagnostics(learningPacMan, new StarterGhosts(), StandardStatistics(), 1, null);
				learningPacMan.UpdateWeights();
				if (stepCount++ > stepSize) {
					stepCount = 0;
					System.out.print("|");
				}
			}
			System.out.println();

			System.out.println("Evaluating after " + ((i+1) * trainGames) + " training games.");
			runDiagnostics(learningPacMan, new StarterGhosts(), StandardStatistics(), testGames,System.out);
			learningPacMan.PrintWeights();
		}
	}
	
	private void TrainPacManController(TrainablePacMan learningPacMan) {
		
		int trainGames = 2000;
		int testGames = 1000;
		int trainIterations = 10;
		
		int stepSize = trainGames / 40;
		int stepCount = 0;
		for (int i = 0; i < trainIterations; i++) {
			
			// how do I make this part abstract?
			 // learningPacMan.ResetMoveCounts();
			
			
			System.out.print("Step " + i + ": Training on " + trainGames + " games: ");
			for (int j = 0; j < trainGames; j++) {
				runDiagnostics(learningPacMan, new StarterGhosts(), StandardStatistics(), 1, null);
				learningPacMan.UpdateTrainingParameters();
				if (stepCount++ > stepSize) {
					stepCount = 0;
					System.out.print("|");
				}
			}
			System.out.println();

			System.out.println("Evaluating after " + ((i+1) * trainGames) + " training games.");
			runDiagnostics(learningPacMan, new StarterGhosts(), StandardStatistics(), testGames,System.out);
			learningPacMan.PrintParameters();
		}
	}
	
    /**
     * For running diagnostic games without visuals
     *
     * @param pacManController The Pac-Man controller
     * @param ghostController The Ghosts controller
     * @param trials The number of trials to be executed
     */
    public static float runDiagnostics(Controller<MOVE> pacManController,Controller<EnumMap<GHOST,MOVE>> ghostController,
    		Collection<Statistic> statistics,int trials,PrintStream stream)
    {
    	Random rnd=new Random(0);
		Game game;
		
		int countSize = trials / 40;
		int counter = 0;
		
		if (stream != null)
			System.out.print("Running test: ");
		
		for(int i=0;i<trials;i++)
		{
			game=new Game(rnd.nextLong());
			
			while(!game.gameOver())
			{
				Game pacmanGameCopy = game.copy();
				Game ghostsGameCopy = game.copy();
				
				long pacManTickStartTime = System.nanoTime(); //System.currentTimeMillis();
				MOVE pacManMove = pacManController.getMove(pacmanGameCopy,System.currentTimeMillis()+DELAY);
				long pacManTickEndTime = System.nanoTime(); //System.currentTimeMillis();
				long ghostsTickStartTime = System.nanoTime(); //System.currentTimeMillis();
				EnumMap<GHOST, MOVE> ghostsMove = ghostController.getMove(ghostsGameCopy,System.currentTimeMillis()+DELAY);
				long ghostsTickEndTime = System.nanoTime(); //System.currentTimeMillis();
				
				game.advanceGame(pacManMove, ghostsMove);
		        for(Statistic stat : statistics) {
		        	stat.aggregateTurnStatistic(game, pacManController, ghostController, pacManTickEndTime-pacManTickStartTime, ghostsTickEndTime-ghostsTickStartTime);
		        }
			}
			
			for(Statistic stat : statistics) {
	        	stat.aggregateGameStatistic(game, pacManController, ghostController);
	        }
			
			if (counter++ >= countSize) {
				counter = 0;
				if (stream != null)
					System.out.print("|");
			}
		}
		if (stream != null)
			System.out.println();
		
		/*
		for(Statistic stat : statistics) {
        	stat.printAverage();
        }
        */
		
		String[][] table = new String[statistics.size()+1][3];
		table[0][0] = "";
		table[0][1] = "Avg";
		table[0][2] = "SD";
		int index = 1;
		for(Statistic stat : statistics) {
			table[index][0] = stat.getStatisticName();
			table[index][1] = new DecimalFormat("#.##").format(stat.getAverage());
			table[index][2] = new DecimalFormat("#.##").format(stat.getStandardDeviation());
			index++;
		}
		if (stream != null) {
			PrettyPrinter pp = new PrettyPrinter(stream);
			pp.print(table);
		}
		
		return 0.0f;
    }
    
    private static void runDiagnostics(Collection<TestPair> testPairs, Collection<Statistic> statistics,int trials) {
    	for (TestPair testPair : testPairs) {
    		java.io.File file = new java.io.File("myData/"+testPair.m_PacManName+"VS"+testPair.m_GhostName+".txt");
    		PrintStream fileStream = null;
    		try { fileStream = new PrintStream(file); }
    		catch (FileNotFoundException exception) {
    			System.out.println("Output file could not be created");
    			System.exit(-1);
    		}
    		
    		runDiagnostics(testPair.PacManController(), testPair.GhostController(), statistics, trials, fileStream);
    		
    		fileStream.close();
    	}
    	System.out.println("Finished!");
    }
    
    /**
     * For comparing the differences between two pacman controllers
     */
    public void runComparison(TestPair pair1, TestPair pair2, int delay, boolean visual)
    {
    	// Random rnd=new Random(0);
		Game game1,game2;
		
		// long gameSeed = rnd.nextLong();
		game1=new Game(0);
		game2=new Game(0);
		
		GameView gv1=null;
		GameView gv2=null;
		if(visual) {
			gv1=new GameView(game1).showGame();
			gv2=new GameView(game2).showGame();
		}
		
		Controller<MOVE> pacManController1 = pair1.PacManController();
		Controller<EnumMap<GHOST,MOVE>> ghostController1 = pair1.GhostController();
		Controller<MOVE> pacManController2 = pair2.PacManController();
		Controller<EnumMap<GHOST,MOVE>> ghostController2 = pair2.GhostController();
		
		boolean pacmansDiffer = false;
		boolean ghostsDiffer  = false;
		
		int iteration = 0;
			
		while(!game1.gameOver() && !game2.gameOver())
		{
			Game pacmanGameCopy1 = game1.copy();
			Game ghostsGameCopy1 = game1.copy();
			Game pacmanGameCopy2 = game2.copy();
			Game ghostsGameCopy2 = game2.copy();
			
			MOVE pacManMove1 = pacManController1.getMove(pacmanGameCopy1,System.currentTimeMillis()+DELAY);
			EnumMap<GHOST, MOVE> ghostsMove1 = ghostController1.getMove(ghostsGameCopy1,System.currentTimeMillis()+DELAY);
			MOVE pacManMove2 = pacManController2.getMove(pacmanGameCopy2,System.currentTimeMillis()+DELAY);
			EnumMap<GHOST, MOVE> ghostsMove2 = ghostController2.getMove(ghostsGameCopy2,System.currentTimeMillis()+DELAY);
			
			if(pacManMove1.compareTo(pacManMove2) != 0) {
				pacmansDiffer = true;
			}
			if(game1.wasPacManEaten() ^ game2.wasPacManEaten()) {
				pacmansDiffer = true;
			}
			for(GHOST ghost : GHOST.values()) {
				boolean move1containsKey = ghostsMove1.containsKey(ghost);
				boolean move2containsKey = ghostsMove2.containsKey(ghost);
				if(move1containsKey && move2containsKey) {
					if(ghostsMove1.get(ghost).compareTo(ghostsMove2.get(ghost)) != 0) {
						ghostsDiffer = true;
					}
				}
				if(move1containsKey ^ move2containsKey) {
					ghostsDiffer = true;
				}
			}
			
			if(pacmansDiffer) {
				// break point here
			}
			if(ghostsDiffer) {
				// break point here
			}
			
			game1.advanceGame(pacManMove1, ghostsMove1);
			game2.advanceGame(pacManMove2, ghostsMove2);
			
	        if(visual) {
	        	gv1.repaint();
	        	gv2.repaint();
	        }
	        
	        if(delay > 0) {
	        	try{Thread.sleep(delay);}catch(Exception e){}
	        }
	        
			iteration++;
		}
    }
    

	public static void saveObject(Object object, String file_name) {
		try {
			ObjectOutputStream oos =
				new ObjectOutputStream(new BufferedOutputStream(
						new FileOutputStream(new File(file_name))));
			oos.writeObject(object);
			oos.close();
		}
		catch (IOException e) {
			System.err.println("Exception writing file " + file_name + ": " + e);
		}
	}

	public static Object loadObject(String file_name) {
		ObjectInputStream ois;
		try {
			ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(new File(file_name))));
			Object object = ois.readObject();
			ois.close();
			return object;
		} catch (IOException e) {
			System.err.println("Error loading: " + file_name);
		} catch (ClassNotFoundException e) {
			System.err.println("Error loading: " + file_name);
		}
		return null;
	}
    
    private class TestPair {
    	
    	public TestPair(Controller<MOVE> pacManController, String pacManName, 
    			Controller<EnumMap<GHOST,MOVE>> ghostController, String ghostName) {
    		m_PacManController = pacManController;
    		m_PacManName = pacManName;
    		m_GhostController = ghostController;
    		m_GhostName = ghostName;
    	}
    	
    	Controller<MOVE> m_PacManController;
    	public Controller<MOVE> PacManController() { return m_PacManController; }
    	
    	String m_PacManName;
    	
    	Controller<EnumMap<GHOST,MOVE>> m_GhostController;
    	public Controller<EnumMap<GHOST,MOVE>> GhostController() { return m_GhostController; }

    	String m_GhostName;
    }
    
	
    public static abstract class Statistic {
    	protected ArrayList<Double> data;
    	protected double count;
    	protected long lcount;
    	protected double last;
    	protected long llast;
    	private double precomputedStdDev;
    	public Statistic() {
    		data = new ArrayList<Double>();
    		count = 0;
    		lcount = 0;
    		precomputedStdDev = -1;
    	}
    	public abstract void aggregateTurnStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController, long pacManTickTime, long ghostsTickTime);
    	public abstract void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController);
    	public double getAverage() {
    		double sum = 0;
    		for(double inst : data) {
    			sum += inst;
    		}
    		double avg = sum / data.size();
    		calcStdDev(avg);
    		return avg;
    	}
    	private void calcStdDev(double avg) {
    		double sum = 0;
    		for(double inst : data) {
    			double diff = inst - avg;
    			sum += diff*diff;
    		}
    		precomputedStdDev = Math.sqrt(sum / data.size());
    	}
    	public double getStandardDeviation() {
    		if(precomputedStdDev < 0) {
    			getAverage();
    		}
    		double sd = precomputedStdDev;
    		precomputedStdDev = -1;
    		return sd;
    	}
    	public void printAverage() {
    		System.out.println(new DecimalFormat("#.##").format(getAverage())+"\t= average " + getStatisticName());
    	}
    	public abstract String getStatisticName();
    }
    
    // collects score of each game
    public static class ScoreStatistic extends Statistic {
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		data.add((double)game.getScore());
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
		}

		@Override
		public String getStatisticName() {
			return "score per game";
		}
    }
    
    // collects # pills eaten for each game
    public static class PillStatistic extends Statistic {
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		int TotalPills = game.getCurrentMaze().pillIndices.length;
    		data.add((double)(TotalPills - game.getActivePillsIndices().length));
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
		}

		@Override
		public String getStatisticName() {
			return "pills eaten per game";
		}
    }
    
    // collects # power pills eaten for each game
    public static class PowerPillStatistic extends Statistic {
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		int TotalPowerPills = game.getCurrentMaze().powerPillIndices.length;
    		data.add((double)(TotalPowerPills - game.getActivePowerPillsIndices().length));
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
		}

		@Override
		public String getStatisticName() {
			return "power pills eaten per game";
		}
    }
    
    // collects total number of ghosts eaten over the entire game
    public static class NumberGhostsEatenStatistic extends Statistic {

    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		data.add(count);
    		count = 0;
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
			for(GHOST ghost : GHOST.values()) {
				if(game.wasGhostEaten(ghost)) count++;
			}
		}

		@Override
		public String getStatisticName() {
			return "ghosts eaten per game";
		}
    }
    
    // collects total number of ghosts eaten over the entire game
    public static class TimePerDeathStatistic extends Statistic {
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
			count++;
			if(game.wasPacManEaten()) {
				data.add(count);
				count = 0;
			}
		}

		@Override
		public String getStatisticName() {
			return "ticks per death";
		}
    }
    
    // collects number of ticks per game
    public static class TicksPerGameStatistic extends Statistic {
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		data.add(count);
			count = 0;
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
			count++;
		}

		@Override
		public String getStatisticName() {
			return "ticks per game";
		}
    }
    
    // collects number of level beaten per game
    public static class TicksPerLevelStatistic extends Statistic {
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		lcount = 0;
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
			
			if(game.getCurrentLevel() != lcount) {
				data.add((double)(llast));
				lcount = game.getCurrentLevel();
			}
			llast = game.getCurrentLevelTime();
		}

		@Override
		public String getStatisticName() {
			return "ticks per level";
		}
    }
    
    // collects number of level beaten per game
    public static class LevelsReachedStatistic extends Statistic {
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		data.add((double)(lcount));
    		lcount = 0;
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
			if(lcount < game.getCurrentLevel()) {
				lcount = game.getCurrentLevel();
			}
		}

		@Override
		public String getStatisticName() {
			return "levels reached per game";
		}
    }
    
    // collects the rate of timeouts per game
    public static class PacManTimeoutRatePerTurnStatistic extends Statistic {
    	private long turnCount = 0;
    	private final long delayNano = DELAY*1000000;
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		data.add(((double)lcount) / turnCount);
    		lcount = 0;
    		turnCount = 0;
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
			if(pacManTickTime > delayNano) {
				lcount++;
			}
			turnCount++;
		}

		@Override
		public String getStatisticName() {
			return "pacman timeout rate per turn";
		}
    }
    
    
    // collects the average time to compute each move for pacman
    public static class PacManAverageTurnTimeStatistic extends Statistic {
    	private long turnCount = 0;
    	
    	@Override
    	public void aggregateGameStatistic(Game game, Controller<MOVE> pacManController, Controller<EnumMap<GHOST,MOVE>> ghostController) {
    		data.add(((double)lcount) / turnCount);
    		lcount = 0;
    		turnCount = 0;
    	}

		@Override
		public void aggregateTurnStatistic(Game game,
				Controller<MOVE> pacManController,
				Controller<EnumMap<GHOST, MOVE>> ghostController, long pacManTickTime, long ghostsTickTime) {
			lcount += pacManTickTime;
			turnCount++;
		}

		@Override
		public String getStatisticName() {
			return "pacman average turn time per game (ns)";
		}
		
    }
}
